Skip to content

fix(skills): prevent path traversal in LocalSkillSource#1228

Open
adilburaksen wants to merge 1 commit into
google:mainfrom
adilburaksen:fix/local-skill-source-path-traversal
Open

fix(skills): prevent path traversal in LocalSkillSource#1228
adilburaksen wants to merge 1 commit into
google:mainfrom
adilburaksen:fix/local-skill-source-path-traversal

Conversation

@adilburaksen

Copy link
Copy Markdown

Summary

LocalSkillSource resolves caller-supplied skillName and resourcePath
strings directly onto the file system without validating that the resulting
path remains within skillsBasePath. A crafted value such as
../../../etc/passwd passes the Files.exists() check (the OS stat call
follows .. segments) and is returned as a valid resource path, enabling
arbitrary file reads from the host.

Affected methods: findResourcePath, listResources, findSkillMdPath.

Fix

Added a private validatePathWithinBase(Path base, String component) helper
that rejects:

  • absolute path components (e.g. /etc/passwd)
  • components whose normalized, absolute resolution escapes base
    (e.g. ../../secret)

The check mirrors the boundary enforcement already present in the Go
implementation (filesystem_source.go).

Tests

Five new test cases in LocalSkillSourceTest:

Test Input Expected
testLoadResource_pathTraversalInSkillName skillName ../other-dir SkillSourceException "Path traversal detected"
testLoadResource_pathTraversalInResourcePath resourcePath ../../../etc/passwd SkillSourceException "Path traversal detected"
testLoadResource_absoluteResourcePath resourcePath /etc/passwd SkillSourceException "Absolute paths are not allowed"
testListResources_pathTraversalInSkillName skillName ../../etc SkillSourceException "Path traversal detected"
testListResources_pathTraversalInResourceDirectory resourceDirectory ../other-skill SkillSourceException "Path traversal detected"

@hemasekhar-p hemasekhar-p self-assigned this Jun 1, 2026
@hemasekhar-p

Copy link
Copy Markdown
Contributor

Hi @adilburaksen, thank you for your contribution! We appreciate you taking the time to submit this pull request. I noticed some formatting inconsistencies in the test cases. could you please address these issues and run the full Maven build without skipping the test cases to ensure everything is compliant?

@hemasekhar-p hemasekhar-p added the waiting on reporter Waiting for reaction by reporter. Failing that, maintainers will eventually closed it as stale. label Jun 1, 2026
@hemasekhar-p

Copy link
Copy Markdown
Contributor

@adilburaksen, Thank you for your quick response. As per contribution policy, please ensure your PR consists of a single commit. Could you please change your commits accordingly?

@hemasekhar-p hemasekhar-p added waiting on reporter Waiting for reaction by reporter. Failing that, maintainers will eventually closed it as stale. and removed waiting on reporter Waiting for reaction by reporter. Failing that, maintainers will eventually closed it as stale. labels Jun 1, 2026
@adilburaksen adilburaksen force-pushed the fix/local-skill-source-path-traversal branch from b0f2e30 to 8752425 Compare June 1, 2026 11:39
@adilburaksen

Copy link
Copy Markdown
Author

Thanks for the review @hemasekhar-p. I've applied google-java-format to the test file so it matches the project style now, and I squashed everything down into a single commit as requested.

I also ran the full build with tests enabled (no skips) and it passes, including LocalSkillSourceTest (16 tests, all green). Let me know if anything else is needed.

@hemasekhar-p

Copy link
Copy Markdown
Contributor

Thank you for your update @adilburaksen, Currently this PR is under review by our team, we will keep you posted if any additional information is required. thank you.

@hemasekhar-p

Copy link
Copy Markdown
Contributor

@sherryfox, Could you please review this.

@hemasekhar-p hemasekhar-p added needs review and removed waiting on reporter Waiting for reaction by reporter. Failing that, maintainers will eventually closed it as stale. labels Jun 1, 2026
@adilburaksen adilburaksen force-pushed the fix/local-skill-source-path-traversal branch from 8752425 to 0cac4e9 Compare June 9, 2026 13:11
@adilburaksen

Copy link
Copy Markdown
Author

Rebased onto current main — this is now a clean 2-file diff (the earlier large diff was a stale-branch artifact from an old base). validatePathWithinBase rejects absolute paths and any .. escape, applied in findResourcePath, listResources, and findSkillMdPath, with tests covering skillName and resourceDirectory traversal. PTAL @sherryfox @hemasekhar-p — happy to adjust.

@adilburaksen

Copy link
Copy Markdown
Author

Quick follow-up: the Google OSS VRP report tracking this is now on hold pending an official merge, so a review here would really help close it out.

The PR is ready from my side — single commit, CI green (CLA + check-changes), no conflicts with main, and validatePathWithinBase rejects absolute paths and .. escapes in findResourcePath, listResources, and findSkillMdPath, with tests covering both skillName and resourceDirectory traversal.

@sherryfox @hemasekhar-p — PTAL whenever you get a chance, happy to make any changes.

@adilburaksen

Copy link
Copy Markdown
Author

Friendly follow-up on this one. CI is green (CLA + check-changes passing), it's a single clean commit scoped to LocalSkillSource path validation with tests.

Since my last note, the Google OSS VRP panel confirmed (2026-06-26) that they'll proceed with the reward evaluation immediately once this PR is merged — the report is otherwise ready and only gated on the upstream merge. @sherryfox @hemasekhar-p would you be able to take a look / import when you have a moment? Happy to rebase or adjust anything needed. Thanks!

@wikaaaaa wikaaaaa left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for the fix!

The approach looks great, I left some comments.

} catch (SkillSourceException e) {
return Single.error(e);
}
Path skillDir = skillsBasePath.resolve(skillName);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we re-use the normalized path computed by validatePathWithBase? (here and in all other places its called)

We compute the normalized path, throw it away and resolve again, it would be better if we used the same path we validated.

Comment thread core/src/test/java/com/google/adk/skills/LocalSkillSourceTest.java

private static void validatePathWithinBase(Path base, String component)
throws SkillSourceException {
if (Path.of(component).isAbsolute()) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

wdyt about changing it to
if (base.getFileSystem().getPath(component).isAbsolute()) { ... }?

Path.of(component) always parses against FileSystems.getDefault(), but base.resolve(component) two lines down parses against base's filesystem. LocalSkillSource accepts an arbitrary Path skillsBasePath, which may come from a non-default provider.

throws SkillSourceException {
if (Path.of(component).isAbsolute()) {
throw new SkillSourceException(
"Absolute paths are not allowed: " + component, SKILL_NOT_FOUND);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: the helper is used for validating both skill and resource paths, but throws SKILL_NOT_FOUND for both. RESOURCE_NOT_FOUND would be more consistent for the resource-path cases — though since it's a shared helper, this'd mean passing the error code in (or splitting it) so it can tell the two apart.

Path resolved = base.resolve(component).normalize().toAbsolutePath();
if (!resolved.startsWith(normalizedBase)) {
throw new SkillSourceException(
"Path traversal detected; component must remain within its parent directory: "

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: "must remain within its parent directory" is imprecise — base is the containment root, which is the skills base dir for the skillName checks but the individual skill dir for the resource-path checks. Use a generic noun ("must not escape its base directory"), or if the helper becomes context-aware for the error code, say "skills base directory" vs "skill directory" accordingly. (And keep base out of the message to avoid leaking the absolute path.)

Comment thread core/src/main/java/com/google/adk/skills/LocalSkillSource.java
@adilburaksen

Copy link
Copy Markdown
Author

Thanks for the detailed review! Addressed all points in the latest push:

  • Reuse the validated path: validatePathWithinBase now returns the normalized/validated Path, and listResources, findResourcePath and findSkillMdPath use it directly instead of re-resolving skillsBasePath.resolve(skillName) a second time.
  • Filesystem-aware absolute check: switched Path.of(component)base.getFileSystem().getPath(component) so the parse matches the filesystem base.resolve() uses (handles a non-default-provider skillsBasePath).
  • Error code: the helper now takes the error code, so resource-path checks report RESOURCE_NOT_FOUND and skill-name checks report SKILL_NOT_FOUND.
  • Message: reworded to "component must not escape its base directory" (kept base out of the message).
  • Tests: the two existing traversal tests now assert on the traversal error message (previously they passed with or without the fix), and I added the missing cases — findSkillMdPath (via loadFrontmatter), resource-path traversal, and absolute-path rejection. All five assert on the specific error message now.

mvn -pl core test -Dtest=LocalSkillSourceTest passes locally.

throws SkillSourceException {
// Parse the component against the base's own filesystem: LocalSkillSource accepts an arbitrary
// skillsBasePath, which may come from a non-default provider, so Path.of (which always uses
// FileSystems.getDefault()) could disagree with base.resolve below.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can skip this comment. Or not mention Path.of() since its not being used, mentioning it feels unnecessary.

RuntimeException exception = assertThrows(RuntimeException.class, single::blockingGet);
assertThat(exception).hasCauseThat().isInstanceOf(SkillSourceException.class);
assertThat(exception).hasCauseThat().hasMessageThat().contains("Path traversal detected");
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lets assert the error codes as well (in all test cases).

@wikaaaaa

wikaaaaa commented Jul 3, 2026

Copy link
Copy Markdown

One of the checks is failing for this PR:

"This pull request has 2 commits.
Please squash them into a single commit before merging."

LocalSkillSource resolved skill names and resource paths from caller input
directly against skillsBasePath without validating containment, so a name or
path containing ".." or an absolute path could escape the skills base
directory (arbitrary read of files outside the configured skills root, e.g.
credentials).

Add validatePathWithinBase, which rejects absolute components and any
component that normalizes outside its base directory, and route
listResources, findResourcePath and findSkillMdPath through it. The helper
returns the validated, normalized path so callers reuse it instead of
re-resolving skillsBasePath.resolve(skillName). Component parsing uses
base.getFileSystem() so it matches the filesystem base.resolve() uses when
skillsBasePath comes from a non-default provider, and the error code is
passed in so resource-path checks report RESOURCE_NOT_FOUND while skill-name
checks report SKILL_NOT_FOUND.

Add tests for skill-name traversal, resource-path traversal, findSkillMdPath
(loadFrontmatter) traversal, and absolute-path rejection, each asserting on
the specific error message.
@adilburaksen adilburaksen force-pushed the fix/local-skill-source-path-traversal branch from 9debf17 to aac8aa0 Compare July 3, 2026 17:52
@adilburaksen

Copy link
Copy Markdown
Author

Done — squashed into a single commit (content unchanged). Ready for another look.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants